// Copyright (c) 2023-2025 FlyByWire Simulations
// SPDX-License-Identifier: GPL-3.0

import { FlightPlanService } from '@fmgc/flightplanning/FlightPlanService';
import { GuidanceController } from '@fmgc/guidance/GuidanceController';
import { A380AircraftConfig } from '@fmgc/flightplanning/A380AircraftConfig';
import {
  ArraySubject,
  ConsumerSubject,
  EventBus,
  MappedSubject,
  SimVarValueType,
  Subject,
  Subscribable,
  Subscription,
} from '@microsoft/msfs-sdk';
import { A380AltitudeUtils } from '@shared/OperatingAltitudes';
import { maxBlockFuel, maxCertifiedAlt, maxZfw } from '@shared/PerformanceConstants';
import { FmgcFlightPhase } from '@shared/flightphase';
import { FmcAircraftInterface } from 'instruments/src/MFD/FMC/FmcAircraftInterface';
import { FmgcDataService } from 'instruments/src/MFD/FMC/fmgc';
import { FmcInterface, FmcOperatingModes } from 'instruments/src/MFD/FMC/FmcInterface';
import {
  DatabaseItem,
  EfisSide,
  FMMessage,
  Fix,
  NXDataStore,
  Units,
  UpdateThrottler,
  VerticalPathCheckpoint,
  Waypoint,
  a380EfisRangeSettings,
} from '@flybywiresim/fbw-sdk';
import {
  isTypeIIMessage,
  McduMessage,
  NXFictionalMessages,
  NXSystemMessages,
  TypeIIMessage,
  TypeIMessage,
} from 'instruments/src/MFD/shared/NXSystemMessages';
import { DataManager, LatLonFormatType, PilotWaypoint } from '@fmgc/flightplanning/DataManager';
import { Coordinates, bearingTo } from 'msfs-geo';
import { FmsDisplayInterface } from '@fmgc/flightplanning/interface/FmsDisplayInterface';
import { MfdDisplayInterface } from 'instruments/src/MFD/MFD';
import { FmcIndex } from 'instruments/src/MFD/FMC/FmcServiceInterface';
import { FmsErrorType } from '@fmgc/FmsError';
import { A380Failure } from '@failures';
import { FpmConfigs } from '@fmgc/flightplanning/FpmConfig';
import { FlightPhaseManager, FlightPhaseManagerEvents } from '@fmgc/flightphase';
import { MfdUIData } from 'instruments/src/MFD/shared/MfdUIData';
import { ActiveUriInformation } from 'instruments/src/MFD/pages/common/MfdUiService';
import { A320FlightPlanPerformanceData } from '@fmgc/flightplanning/plans/performance/FlightPlanPerformanceData';
import { EfisInterface } from '@fmgc/efis/EfisInterface';
import { Navigation } from '@fmgc/navigation/Navigation';
import { EfisSymbols } from '@fmgc/efis/EfisSymbols';
import { FlightPlanIndex } from '@fmgc/flightplanning/FlightPlanManager';
import { NavigationDatabase, NavigationDatabaseBackend } from '@fmgc/NavigationDatabase';
import { NavigationDatabaseService } from '@fmgc/flightplanning/NavigationDatabaseService';
import { ReadonlyFlightPlan } from '@fmgc/flightplanning/plans/ReadonlyFlightPlan';
import { VdAltitudeConstraint } from 'instruments/src/MsfsAvionicsCommon/providers/MfdSurvPublisher';

export interface FmsErrorMessage {
  message: McduMessage;
  messageText: string;
  backgroundColor: 'white' | 'amber' | 'cyan'; // Whether the message should be colored.
  cleared: boolean; // If message has been cleared from footer
  isResolvedOverride: () => boolean;
  onClearOverride: () => void;
}

export const FMS_CYCLE_TIME = 250; // ms

/*
 * Handles navigation (and potentially other aspects) for MFD pages
 */
export class FlightManagementComputer implements FmcInterface {
  protected readonly subs = [] as Subscription[];

  private readonly ndMessageFlags: Record<'L' | 'R', number> = {
    L: 0,
    R: 0,
  };

  #mfdReference: (FmsDisplayInterface & MfdDisplayInterface) | null;

  get mfdReference() {
    return this.#mfdReference;
  }

  set mfdReference(value: (FmsDisplayInterface & MfdDisplayInterface) | null) {
    this.#mfdReference = value;

    if (value) {
      this.dataManager = new DataManager(value, { latLonFormat: LatLonFormatType.ExtendedFormat });
    }
  }

  #operatingMode: FmcOperatingModes;

  get operatingMode(): FmcOperatingModes {
    // TODO amend once
    return this.#operatingMode;
  }

  set operatingMode(value: FmcOperatingModes) {
    this.#operatingMode = value;
  }

  // FIXME A320 data
  #flightPlanService = new FlightPlanService(this.bus, new A320FlightPlanPerformanceData(), FpmConfigs.A380);

  get flightPlanService() {
    return this.#flightPlanService;
  }

  private lastFlightPlanVersion: number | null = null;

  #fmgc = new FmgcDataService(this.flightPlanService);

  get fmgc() {
    return this.#fmgc;
  }

  private fmsUpdateThrottler = new UpdateThrottler(FMS_CYCLE_TIME);

  private efisInterfaces = {
    L: new EfisInterface('L', this.flightPlanService),
    R: new EfisInterface('R', this.flightPlanService),
  };

  #guidanceController!: GuidanceController;

  get guidanceController() {
    return this.#guidanceController;
  }

  #navigation: Navigation;

  get navigation() {
    if (this.instance !== FmcIndex.FmcA) {
      throw new Error('Multiple navigation instances not supported!');
    }
    return this.#navigation;
  }

  get navaidTuner() {
    if (this.instance !== FmcIndex.FmcA) {
      throw new Error('Multiple navaid tuners not supported!');
    }
    return this.#navigation.getNavaidTuner();
  }

  private efisSymbolsLeft!: EfisSymbols<number>;
  private efisSymbolsRight!: EfisSymbols<number>;

  private readonly flightPhaseManager = new FlightPhaseManager(this.bus);

  // TODO remove this cyclic dependency, isWaypointInUse should be moved to DataInterface
  private dataManager: DataManager | null = null;

  private readonly sub = this.bus.getSubscriber<FlightPhaseManagerEvents & MfdUIData>();

  private readonly flightPhase = ConsumerSubject.create<FmgcFlightPhase>(
    this.sub.on('fmgc_flight_phase'),
    this.flightPhaseManager.phase,
  );
  private readonly activePage = ConsumerSubject.create<ActiveUriInformation | null>(
    this.sub.on('mfd_active_uri'),
    null,
  );

  private readonly isReset = Subject.create(true);

  private readonly shouldBePreflightPhase = MappedSubject.create(
    ([phase, page, isReset]) => {
      const isPreflight =
        phase === FmgcFlightPhase.Done &&
        isReset &&
        (page?.uri === 'fms/active/init' || page?.uri === 'fms/active/fuel-load' || page?.uri === 'fms/active/perf');
      return isPreflight;
    },
    this.flightPhase,
    this.activePage,
    this.isReset,
  );

  public getDataManager() {
    return this.dataManager;
  }

  #fmsErrors = ArraySubject.create<FmsErrorMessage>();

  get fmsErrors() {
    return this.#fmsErrors;
  }

  // TODO make private, and access methods through FmcInterface
  public acInterface!: FmcAircraftInterface;

  public revisedLegIndex = Subject.create<number | null>(null);

  public revisedLegPlanIndex = Subject.create<FlightPlanIndex | null>(null);

  public revisedLegIsAltn = Subject.create<boolean | null>(null);

  public enginesWereStarted = Subject.create<boolean>(false);

  private readonly failureKey =
    this.instance === FmcIndex.FmcA
      ? A380Failure.FmcA
      : this.instance === FmcIndex.FmcB
        ? A380Failure.FmcB
        : A380Failure.FmcC;

  private readonly legacyFmsIsHealthy = Subject.create(false);

  private wasReset = false;

  private readonly ZfwOrZfwCgUndefined = MappedSubject.create(
    ([zfw, zfwCg]) => zfw == null || zfwCg == null,
    this.fmgc.data.zeroFuelWeight,
    this.fmgc.data.zeroFuelWeightCenterOfGravity,
  );

  private readonly destDataEntered = MappedSubject.create(
    ([qnh, temperature, wind]) => qnh !== null && temperature !== null && wind !== null,
    this.fmgc.data.approachQnh,
    this.fmgc.data.approachTemperature,
    this.fmgc.data.approachWind,
  );

  private destDataCheckedInCruise = false;

  constructor(
    private instance: FmcIndex,
    operatingMode: FmcOperatingModes,
    private readonly bus: EventBus,
    private readonly fmcInop: Subscribable<boolean>,
    mfdReference: (FmsDisplayInterface & MfdDisplayInterface) | null,
  ) {
    this.#operatingMode = operatingMode;
    this.#mfdReference = mfdReference;

    const db = new NavigationDatabase(this.bus, NavigationDatabaseBackend.Msfs);
    NavigationDatabaseService.activeDatabase = db;

    this.#navigation = new Navigation(this.bus, this.flightPlanService);

    this.flightPlanService.createFlightPlans();

    // FIXME implement sync between FMCs and also let FMC-B and FMC-C compute
    if (this.instance === FmcIndex.FmcA) {
      this.acInterface = new FmcAircraftInterface(this.bus, this, this.fmgc, this.flightPlanService);

      this.#guidanceController = new GuidanceController(
        this.bus,
        this.fmgc,
        this.flightPlanService,
        this.efisInterfaces,
        a380EfisRangeSettings,
        A380AircraftConfig,
      );
      this.efisSymbolsLeft = new EfisSymbols(
        this.bus,
        'L',
        this.#guidanceController,
        this.flightPlanService,
        this.navaidTuner,
        this.efisInterfaces.L,
        a380EfisRangeSettings,
        true,
      );
      this.efisSymbolsRight = new EfisSymbols(
        this.bus,
        'R',
        this.#guidanceController,
        this.flightPlanService,
        this.navaidTuner,
        this.efisInterfaces.R,
        a380EfisRangeSettings,
        true,
      );

      this.#navigation.init();
      this.efisSymbolsLeft.init();
      this.efisSymbolsRight.init();
      this.flightPhaseManager.init();
      this.#guidanceController.init();
      this.fmgc.guidanceController = this.#guidanceController;

      this.initSimVars();

      this.flightPhaseManager.addOnPhaseChanged((prev, next) => this.onFlightPhaseChanged(prev, next));

      this.subs.push(
        this.shouldBePreflightPhase.sub((shouldBePreflight) => {
          if (shouldBePreflight) {
            this.flightPhaseManager.changePhase(FmgcFlightPhase.Preflight);
          }
        }, true),
        this.enginesWereStarted.sub((val) => {
          if (
            val &&
            this.flightPlanService.hasActive &&
            !Number.isFinite(this.flightPlanService.active.performanceData.costIndex.get())
          ) {
            this.flightPlanService.active.setPerformanceData('costIndex', 0);
            this.addMessageToQueue(NXSystemMessages.costIndexInUse.getModifiedMessage('000'));
          }
        }),
        this.legacyFmsIsHealthy.sub((v) => {
          // FIXME some of the systems require the A320 FMS health bits, need to refactor/split FMS stuff
          SimVar.SetSimVarValue('L:A32NX_FM1_HEALTHY_DISCRETE', 'boolean', v);
          SimVar.SetSimVarValue('L:A32NX_FM2_HEALTHY_DISCRETE', 'boolean', v);
        }, true),

        this.ZfwOrZfwCgUndefined,
        this.ZfwOrZfwCgUndefined.sub((v) => {
          if (!v) {
            this.removeMessageFromQueue(NXSystemMessages.initializeZfwOrZfwCg.text);
          }
        }),

        this.fmgc.data.cpnyFplnAvailable.sub((v) => {
          if (v) {
            this.addMessageToQueue(NXSystemMessages.comFplnRecievedPendingInsertion);
          } else {
            this.removeMessageFromQueue(NXSystemMessages.comFplnRecievedPendingInsertion.text);
          }
        }),
        this.destDataEntered,
        this.destDataEntered.sub((v) => {
          if (v) {
            this.removeMessageFromQueue(NXSystemMessages.enterDestData.text);
          }
        }),
      );

      this.subs.push(this.shouldBePreflightPhase, this.flightPhase, this.activePage);

      this.subs.push(
        this.fmgc.data.engineOut.sub((eo) => {
          if (eo) {
            this.enterEngineOut();
          } else {
            this.exitEngineOut();
          }
        }),
      );
    }

    let lastUpdateTime = Date.now();
    setInterval(() => {
      const now = Date.now();
      const dt = now - lastUpdateTime;

      this.onUpdate(dt);

      lastUpdateTime = now;
    }, 100);

    console.log(`${FmcIndex[this.instance]} initialized.`);
  }

  destroy() {
    for (const s of this.subs) {
      s.destroy();
    }
  }

  public revisedWaypoint(): Fix | undefined {
    const revWptIdx = this.revisedLegIndex.get();
    const revPlanIdx = this.revisedLegPlanIndex.get();
    if (revWptIdx !== null && revPlanIdx !== null && this.flightPlanService.has(revPlanIdx)) {
      const flightPlan = this.revisedLegIsAltn.get()
        ? this.flightPlanService.get(revPlanIdx).alternateFlightPlan
        : this.flightPlanService.get(revPlanIdx);
      if (flightPlan.hasElement(revWptIdx) && flightPlan.elementAt(revWptIdx)?.isDiscontinuity === false) {
        return flightPlan.legElementAt(revWptIdx)?.definition?.waypoint;
      }
    }
    return undefined;
  }

  public setRevisedWaypoint(index: number, planIndex: number, isAltn: boolean) {
    this.revisedLegPlanIndex.set(planIndex);
    this.revisedLegIsAltn.set(isAltn);
    this.revisedLegIndex.set(index);
  }

  public resetRevisedWaypoint(): void {
    this.revisedLegIndex.set(null);
    this.revisedLegIsAltn.set(null);
    this.revisedLegPlanIndex.set(null);
  }

  public latLongStoredWaypoints: Waypoint[] = [];

  public pbdStoredWaypoints: Waypoint[] = [];

  public pbxStoredWaypoints: Waypoint[] = [];

  public deleteAllStoredWaypoints() {
    this.latLongStoredWaypoints = [];
    this.pbdStoredWaypoints = [];
    this.pbxStoredWaypoints = [];
  }

  /**
   * Checks whether a waypoint is currently in use
   * @param waypoint the waypoint to look for
   */
  async isWaypointInUse(waypoint: Waypoint): Promise<boolean> {
    // Check in all flight plans
    if (this.flightPlanService.hasActive) {
      return this.flightPlanService.isWaypointInUse(waypoint);
    }

    return false;
  }

  /** in kg */
  public getLandingWeight(): number | null {
    const tow = this.getTakeoffWeight();
    const gw = this.fmgc.getGrossWeightKg();
    const tf = this.getTripFuel();

    if (!this.enginesWereStarted.get()) {
      // On ground, engines off
      // LW = TOW - TRIP
      return tow && tf ? tow - tf : null;
    }
    if (gw && tf && this.fmgc.getFlightPhase() >= FmgcFlightPhase.Takeoff) {
      // In flight
      // LW = GW - TRIP
      return gw - tf;
    }
    if (gw) {
      // Preflight, engines on
      // LW = GW - TRIP - TAXI
      return gw - (tf ?? 0) - (this.fmgc.data.taxiFuel.get() ?? 0);
    }
    return null;
  }

  public getTakeoffWeight(): number | null {
    if (!this.enginesWereStarted.get()) {
      // On ground, engines off
      // TOW before engine start: TOW = ZFW + BLOCK - TAXI
      const zfw = this.fmgc.data.zeroFuelWeight.get() ?? maxZfw;
      if (this.fmgc.data.zeroFuelWeight.get() && this.fmgc.data.blockFuel.get() && this.fmgc.data.taxiFuel.get()) {
        return zfw + (this.fmgc.data.blockFuel.get() ?? maxBlockFuel) - (this.fmgc.data.taxiFuel.get() ?? 0);
      }
      return null;
    }
    if (this.fmgc.getFlightPhase() >= FmgcFlightPhase.Takeoff) {
      // In flight
      // TOW: TOW = GW
      return SimVar.GetSimVarValue('TOTAL WEIGHT', 'kilogram');
    }
    // Preflight, engines on
    // LW = GW - TRIP - TAXI
    // TOW after engine start: TOW = GW - TAXI
    return SimVar.GetSimVarValue('TOTAL WEIGHT', 'kilogram') - (this.fmgc.data.taxiFuel.get() ?? 0);
  }

  public getTripFuel(): number | null {
    const destPred = this.guidanceController.vnavDriver.getDestinationPrediction();
    if (destPred) {
      const fob = this.fmgc.getFOB()! * 1_000;
      const destFuelKg = Units.poundToKilogram(destPred.estimatedFuelOnBoard);
      return fob - destFuelKg;
    }
    return null;
  }

  public getExtraFuel(): number | null {
    const destPred = this.guidanceController.vnavDriver.getDestinationPrediction();
    if (destPred) {
      if (this.flightPhase.get() === FmgcFlightPhase.Preflight) {
        // EXTRA = BLOCK - TAXI - TRIP - MIN FUEL DEST - RTE RSV
        return (
          (this.enginesWereStarted.get() ? this.fmgc.getFOB()! * 1_000 : this.fmgc.data.blockFuel.get() ?? 0) -
          (this.fmgc.data.taxiFuel.get() ?? 0) -
          (this.getTripFuel() ?? 0) -
          (this.fmgc.data.minimumFuelAtDestination.get() ?? 0) -
          (this.fmgc.data.routeReserveFuelWeight.get() ?? 0)
        );
      } else {
        return (
          Units.poundToKilogram(destPred.estimatedFuelOnBoard) - (this.fmgc.data.minimumFuelAtDestination.get() ?? 0)
        );
      }
    }

    return null;
  }

  /** @inheritdoc */
  public getRecMaxFlightLevel(grossWeight?: number): number | null {
    const gw = grossWeight ?? this.fmgc.getGrossWeightKg();
    if (!gw) {
      return null;
    }

    const isaTempDeviation = A380AltitudeUtils.getIsaTempDeviation();
    return Math.min(A380AltitudeUtils.calculateRecommendedMaxAltitude(gw, isaTempDeviation), maxCertifiedAlt) / 100;
  }

  /** @inheritdoc */
  public getOptFlightLevel(): number | null {
    return Math.floor((0.96 * (this.getRecMaxFlightLevel() ?? maxCertifiedAlt / 100)) / 5) * 5; // FIXME remove magic
  }

  /** @inheritdoc */
  public getEoMaxFlightLevel(): number | null {
    return Math.floor((0.8 * (this.getRecMaxFlightLevel() ?? maxCertifiedAlt / 100)) / 5) * 5; // FIXME remove magic
  }

  private initSimVars() {
    // Reset SimVars
    SimVar.SetSimVarValue('L:A32NX_SPEEDS_MANAGED_PFD', 'knots', 0);
    SimVar.SetSimVarValue('L:A32NX_SPEEDS_MANAGED_ATHR', 'knots', 0);

    SimVar.SetSimVarValue('L:A32NX_MachPreselVal', 'mach', -1);
    SimVar.SetSimVarValue('L:A32NX_SpeedPreselVal', 'knots', -1);

    SimVar.SetSimVarValue('L:AIRLINER_DECISION_HEIGHT', 'feet', -1);
    SimVar.SetSimVarValue('L:AIRLINER_MINIMUM_DESCENT_ALTITUDE', 'feet', 0);

    SimVar.SetSimVarValue('L:A32NX_FG_ALTITUDE_CONSTRAINT', 'feet', 0);
    SimVar.SetSimVarValue('L:A32NX_TO_CONFIG_NORMAL', 'Bool', 0);
    SimVar.SetSimVarValue('L:A32NX_CABIN_READY', 'Bool', 0);
    SimVar.SetSimVarValue('L:A32NX_FM_GROSS_WEIGHT', 'Number', 0);

    if (SimVar.GetSimVarValue('L:A32NX_AUTOTHRUST_DISABLED', 'number') === 1) {
      SimVar.SetSimVarValue('K:A32NX.ATHR_RESET_DISABLE', 'number', 1);
    }

    SimVar.SetSimVarValue('L:A32NX_PFD_MSG_SET_HOLD_SPEED', 'bool', false);

    // Reset SimVars
    SimVar.SetSimVarValue('L:AIRLINER_V1_SPEED', 'Knots', NaN);
    SimVar.SetSimVarValue('L:AIRLINER_V2_SPEED', 'Knots', NaN);
    SimVar.SetSimVarValue('L:AIRLINER_VR_SPEED', 'Knots', NaN);

    const gpsDriven = SimVar.GetSimVarValue('GPS DRIVES NAV1', 'Bool');
    if (!gpsDriven) {
      SimVar.SetSimVarValue('K:TOGGLE_GPS_DRIVES_NAV1', 'Bool', 0);
    }
    SimVar.SetSimVarValue('K:VS_SLOT_INDEX_SET', 'number', 1);

    // Start the check routine for system health and status
    setInterval(() => {
      if (this.flightPhaseManager.phase === FmgcFlightPhase.Cruise && !this.destDataCheckedInCruise) {
        const destPred = this.guidanceController.vnavDriver.getDestinationPrediction();
        if (destPred && Number.isFinite(destPred.distanceFromAircraft) && destPred.distanceFromAircraft < 180) {
          this.destDataCheckedInCruise = true;
          this.checkDestData();
        }
      }
    }, 15000);
  }

  public clearLatestFmsErrorMessage() {
    const arr = this.fmsErrors.getArray();
    const index = arr.findIndex((val) => !val.cleared);

    if (index > -1) {
      if (arr[index].message.isTypeTwo) {
        const old = arr[index];
        old.cleared = true;

        this.fmsErrors.set(arr);
        if (old.onClearOverride) {
          old.onClearOverride();
        }
      } else {
        this.fmsErrors.removeAt(index);
      }
    }
  }

  /**
   * Called when a flight plan uplink is in progress
   */
  onUplinkInProgress() {
    this.fmgc.data.cpnyFplnUplinkInProgress.set(true);
  }

  /**
   * Called when a flight plan uplink is done
   */
  onUplinkDone() {
    this.fmgc.data.cpnyFplnUplinkInProgress.set(false);
    this.fmgc.data.cpnyFplnAvailable.set(true);
  }

  /**
   * Calling this function with a message should display the message in the FMS' message area,
   * such as the scratchpad or a dedicated error line. The FMS error type given should be translated
   * into the appropriate message for the UI
   *
   * @param errorType the message to show
   */
  showFmsErrorMessage(errorType: FmsErrorType) {
    switch (errorType) {
      case FmsErrorType.EntryOutOfRange:
        this.addMessageToQueue(NXSystemMessages.entryOutOfRange, undefined, undefined);
        break;
      case FmsErrorType.FormatError:
        this.addMessageToQueue(NXSystemMessages.formatError, undefined, undefined);
        break;
      case FmsErrorType.NotInDatabase:
        this.addMessageToQueue(NXSystemMessages.notInDatabase, undefined, undefined);
        break;
      case FmsErrorType.NotYetImplemented:
        this.addMessageToQueue(NXFictionalMessages.notYetImplemented, undefined, undefined);
        break;
      default:
        break;
    }
  }

  /**
   * Duplicate implementation, because WaypointEntryUtils needs one parameter with both DataInterface and DisplayInterface
   */
  async deduplicateFacilities<T extends DatabaseItem<any>>(items: T[]): Promise<T | undefined> {
    return this.mfdReference?.deduplicateFacilities(items);
  }

  /**
   * Duplicate implementation, because WaypointEntryUtils needs one parameter with both DataInterface and DisplayInterface
   */
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  async createNewWaypoint(ident: string): Promise<Waypoint | undefined> {
    // TODO navigate to DATA/NAVAID --> PILOT STORED NAVAIDS --> NEW NAVAID
    return undefined;
  }

  /**
   * Add message to fmgc message queue
   * @param _message MessageObject
   * @param _isResolvedOverride Function that determines if the error is resolved at this moment (type II only).
   * @param _onClearOverride Function that executes when the error is actively cleared by the pilot (type II only).
   */
  public addMessageToQueue(
    _message: TypeIMessage | TypeIIMessage,
    _isResolvedOverride: (() => boolean) | undefined = undefined,
    _onClearOverride: (() => void) | undefined = undefined,
  ) {
    const message =
      _isResolvedOverride === undefined && _onClearOverride === undefined
        ? _message
        : _message.getModifiedMessage('', _isResolvedOverride, _onClearOverride);

    const msg: FmsErrorMessage = {
      message: _message,
      messageText: message.text,
      backgroundColor: message.isAmber ? 'amber' : 'white',
      cleared: false,
      onClearOverride: isTypeIIMessage(message) ? message.onClear : () => {},
      isResolvedOverride: isTypeIIMessage(message) ? message.isResolved : () => false,
    };

    const exists = this.fmsErrors.getArray().findIndex((el) => el.messageText === msg.messageText && el.cleared);
    if (exists !== -1) {
      this.fmsErrors.removeAt(exists);
    }
    this.fmsErrors.insert(msg, 0);
  }

  /**
   * Removes a message from the queue
   * @param value {String}
   */
  removeMessageFromQueue(value: string) {
    const exists = this.fmsErrors.getArray().findIndex((el) => el.messageText === value);
    if (exists !== -1) {
      this.fmsErrors.removeAt(exists);
    }
  }

  private updateMessageQueue() {
    this.fmsErrors.getArray().forEach((it, idx) => {
      if (it.message.isTypeTwo && it.isResolvedOverride()) {
        console.warn(`message "${it.messageText}" is resolved.`);
        this.fmsErrors.removeAt(idx);
      }
    });
  }

  openMessageList() {
    this.mfdReference?.openMessageList();
  }

  createLatLonWaypoint(coordinates: Coordinates, stored: boolean, ident?: string): PilotWaypoint | null {
    return this.dataManager?.createLatLonWaypoint(coordinates, stored, ident) ?? null;
  }

  createPlaceBearingPlaceBearingWaypoint(
    place1: Fix,
    bearing1: DegreesTrue,
    place2: Fix,
    bearing2: DegreesTrue,
    stored?: boolean,
    ident?: string,
  ): PilotWaypoint | null {
    return (
      this.dataManager?.createPlaceBearingPlaceBearingWaypoint(place1, bearing1, place2, bearing2, stored, ident) ??
      null
    );
  }

  createPlaceBearingDistWaypoint(
    place: Fix,
    bearing: DegreesTrue,
    distance: NauticalMiles,
    stored?: boolean,
    ident?: string,
  ): PilotWaypoint | null {
    return this.dataManager?.createPlaceBearingDistWaypoint(place, bearing, distance, stored, ident) ?? null;
  }

  getStoredWaypointsByIdent(ident: string): PilotWaypoint[] {
    return this.dataManager?.getStoredWaypointsByIdent(ident) ?? [];
  }

  /**
   * This method is called by the FlightPhaseManager after a flight phase change
   * This method initializes AP States, initiates CDUPerformancePage changes and other set other required states
   * @param prevPhase Previous FmgcFlightPhase
   * @param nextPhase New FmgcFlightPhase
   */
  onFlightPhaseChanged(prevPhase: FmgcFlightPhase, nextPhase: FmgcFlightPhase) {
    this.acInterface.updateConstraints();
    this.acInterface.updateManagedSpeed();

    SimVar.SetSimVarValue('L:A32NX_CABIN_READY', 'Bool', 0);
    this.isReset.set(false);

    switch (nextPhase) {
      case FmgcFlightPhase.Takeoff: {
        this.destDataCheckedInCruise = false;

        const plan = this.flightPlanService.active;
        const pd = this.fmgc.data;

        if (!plan.performanceData.accelerationAltitude.get()) {
          // it's important to set this immediately as we don't want to immediately sequence to the climb phase
          plan.setPerformanceData(
            'pilotAccelerationAltitude',
            SimVar.GetSimVarValue('INDICATED ALTITUDE', 'feet') +
              parseInt(NXDataStore.getLegacy('CONFIG_ACCEL_ALT', '1500')),
          );
          this.acInterface.updateThrustReductionAcceleration();
        }
        if (!plan.performanceData.engineOutAccelerationAltitude.get()) {
          // it's important to set this immediately as we don't want to immediately sequence to the climb phase
          plan.setPerformanceData(
            'pilotEngineOutAccelerationAltitude',
            SimVar.GetSimVarValue('INDICATED ALTITUDE', 'feet') +
              parseInt(NXDataStore.getLegacy('CONFIG_ACCEL_ALT', '1500')),
          );
          this.acInterface.updateThrustReductionAcceleration();
        }

        pd.taxiFuelPilotEntry.set(null);
        pd.defaultTaxiFuel.set(null);
        pd.routeReserveFuelWeightPilotEntry.set(null);
        pd.routeReserveFuelPercentagePilotEntry.set(0);
        pd.routeReserveFuelWeightCalculated.set(0);

        this.fmgc.data.climbPredictionsReferenceAutomatic.set(
          this.guidanceController.verticalProfileComputationParametersObserver.get().fcuAltitude,
        );

        /** Arm preselected speed/mach for next flight phase */
        const climbPreSel = this.fmgc.data.climbPreSelSpeed.get();
        if (climbPreSel) {
          this.acInterface.updatePreSelSpeedMach(climbPreSel);
        }

        break;
      }

      case FmgcFlightPhase.Climb: {
        this.destDataCheckedInCruise = false;

        /** Activate pre selected speed/mach */
        if (prevPhase === FmgcFlightPhase.Takeoff) {
          const climbPreSel = this.fmgc.data.climbPreSelSpeed.get();
          if (climbPreSel) {
            this.acInterface.activatePreSelSpeedMach(climbPreSel);
          }
        }

        /** Arm preselected speed/mach for next flight phase */
        const cruisePreSel = this.fmgc.data.cruisePreSelMach.get() ?? 280;
        const cruisePreSelMach = this.fmgc.data.cruisePreSelMach.get();
        this.acInterface.updatePreSelSpeedMach(cruisePreSelMach ?? cruisePreSel);

        if (!this.flightPlanService.active.performanceData.cruiseFlightLevel.get()) {
          this.flightPlanService.active.setPerformanceData(
            'cruiseFlightLevel',
            (Simplane.getAutoPilotDisplayedAltitudeLockValue('feet') ?? 0) / 100,
          );
          SimVar.SetSimVarValue(
            'L:A32NX_AIRLINER_CRUISE_ALTITUDE',
            'number',
            Simplane.getAutoPilotDisplayedAltitudeLockValue('feet') ?? 0,
          );
        }

        break;
      }

      case FmgcFlightPhase.Cruise: {
        this.destDataCheckedInCruise = false;
        SimVar.SetSimVarValue('L:A32NX_GOAROUND_PASSED', 'bool', 0);

        const cruisePreSel = this.fmgc.data.cruisePreSelSpeed.get();
        const cruisePreSelMach = this.fmgc.data.cruisePreSelMach.get();
        const preSelToActivate = cruisePreSelMach ?? cruisePreSel;

        /** Activate pre selected speed/mach */
        if (prevPhase === FmgcFlightPhase.Climb && preSelToActivate) {
          this.triggerCheckSpeedModeMessage(preSelToActivate);
          this.acInterface.activatePreSelSpeedMach(preSelToActivate);
        }

        /** Arm preselected speed/mach for next flight phase */
        const desPreSel = this.fmgc.data.descentPreSelSpeed.get();
        if (desPreSel) {
          this.acInterface.updatePreSelSpeedMach(desPreSel);
        }

        break;
      }

      case FmgcFlightPhase.Descent: {
        this.checkDestData();

        /** Activate pre selected speed/mach */
        const desPreSel = this.fmgc.data.descentPreSelSpeed.get();
        if (prevPhase === FmgcFlightPhase.Cruise && desPreSel) {
          this.acInterface.activatePreSelSpeedMach(desPreSel);
        }

        this.triggerCheckSpeedModeMessage(null);

        /** Clear pre selected speed/mach */
        this.acInterface.updatePreSelSpeedMach(null);
        this.flightPlanService.active.setPerformanceData('cruiseFlightLevel', null);

        break;
      }

      case FmgcFlightPhase.Approach: {
        SimVar.SetSimVarValue('L:A32NX_GOAROUND_PASSED', 'bool', 0);

        this.checkDestData();

        break;
      }

      case FmgcFlightPhase.GoAround: {
        SimVar.SetSimVarValue('L:A32NX_GOAROUND_INIT_SPEED', 'number', Simplane.getIndicatedSpeed());

        this.flightPlanService.stringMissedApproach(
          /** @type {FlightPlanLeg} */ (map) => {
            this.addMessageToQueue(NXSystemMessages.cstrDelUpToWpt.getModifiedMessage(map.ident));
          },
        );

        const activePlan = this.flightPlanService.active;
        if (!activePlan.performanceData.missedAccelerationAltitude.get()) {
          // it's important to set this immediately as we don't want to immediately sequence to the climb phase
          activePlan.setPerformanceData(
            'pilotMissedAccelerationAltitude',
            SimVar.GetSimVarValue('INDICATED ALTITUDE', 'feet') +
              parseInt(NXDataStore.getLegacy('CONFIG_ENG_OUT_ACCEL_ALT', '1500')),
          );
          this.acInterface.updateThrustReductionAcceleration();
        }
        if (!activePlan.performanceData.missedEngineOutAccelerationAltitude.get()) {
          // it's important to set this immediately as we don't want to immediately sequence to the climb phase
          activePlan.setPerformanceData(
            'pilotMissedEngineOutAccelerationAltitude',
            SimVar.GetSimVarValue('INDICATED ALTITUDE', 'feet') +
              parseInt(NXDataStore.getLegacy('CONFIG_ENG_OUT_ACCEL_ALT', '1500')),
          );
          this.acInterface.updateThrustReductionAcceleration();
        }

        break;
      }

      case FmgcFlightPhase.Done:
        this.flightPlanService
          .reset()
          .then(() => {
            this.fmgc.data.reset();
            this.initSimVars();
            this.deleteAllStoredWaypoints();
            this.clearLatestFmsErrorMessage();
            SimVar.SetSimVarValue('L:A32NX_COLD_AND_DARK_SPAWN', 'Bool', true).then(() => {
              this.mfdReference?.uiService.navigateTo('fms/data/status');
              this.isReset.set(true);
            });
          })
          .catch(console.error);
        break;

      default:
        break;
    }
  }

  triggerCheckSpeedModeMessage(preselectedSpeed: number | null) {
    const isSpeedSelected = !Simplane.getAutoPilotAirspeedManaged();
    const hasPreselectedSpeed = !!preselectedSpeed;

    const checkSpeedModeMessageActive =
      this.fmsErrors.getArray().filter((it) => it.message === NXSystemMessages.checkSpeedMode).length > 0;
    if (!checkSpeedModeMessageActive && isSpeedSelected && !hasPreselectedSpeed) {
      this.addMessageToQueue(
        NXSystemMessages.checkSpeedMode,
        () => !checkSpeedModeMessageActive,
        () => {
          SimVar.SetSimVarValue('L:A32NX_PFD_MSG_CHECK_SPEED_MODE', 'bool', false);
        },
      );
      SimVar.SetSimVarValue('L:A32NX_PFD_MSG_CHECK_SPEED_MODE', 'bool', true);
    }
  }

  clearCheckSpeedModeMessage() {
    const checkSpeedModeMessageActive =
      this.fmsErrors.getArray().filter((it) => it.message === NXSystemMessages.checkSpeedMode).length > 0;
    if (checkSpeedModeMessageActive && Simplane.getAutoPilotAirspeedManaged()) {
      this.removeMessageFromQueue(NXSystemMessages.checkSpeedMode.text);
      SimVar.SetSimVarValue('L:A32NX_PFD_MSG_CHECK_SPEED_MODE', 'bool', false);
    }
  }

  private checkDestData(): void {
    if (!this.destDataEntered.get()) {
      this.addMessageToQueue(NXSystemMessages.enterDestData);
    }
  }

  private zfwInitDisplayed = 0;

  private initMessageSettable = false;

  private checkZfwParams(): void {
    const eng2state = SimVar.GetSimVarValue('L:A32NX_ENGINE_STATE:2', 'Number');
    const eng3state = SimVar.GetSimVarValue('L:A32NX_ENGINE_STATE:3', 'Number');

    if (eng2state === 2 || eng3state === 2) {
      if (this.zfwInitDisplayed < 1 && this.flightPhaseManager.phase < FmgcFlightPhase.Takeoff) {
        this.initMessageSettable = true;
      }
    }
    // INITIALIZE ZFW/ZFW CG
    if (this.fmgc.isAnEngineOn() && this.ZfwOrZfwCgUndefined.get() && this.initMessageSettable) {
      this.addMessageToQueue(NXSystemMessages.initializeZfwOrZfwCg);
      this.zfwInitDisplayed++;
      this.initMessageSettable = false;
    }
  }

  private onUpdate(dt: number) {
    const isHealthy = !this.fmcInop.get();

    SimVar.SetSimVarValue(
      `L:A32NX_FMC_${this.instance === FmcIndex.FmcA ? 'A' : this.instance === FmcIndex.FmcB ? 'B' : 'C'}_IS_HEALTHY`,
      SimVarValueType.Bool,
      isHealthy,
    );

    // Stop early, if not FmcA or if all FMCs failed
    const allFmcResetsPulled =
      SimVar.GetSimVarValue('L:A32NX_RESET_PANEL_FMC_A', SimVarValueType.Bool) &&
      SimVar.GetSimVarValue('L:A32NX_RESET_PANEL_FMC_B', SimVarValueType.Bool) &&
      SimVar.GetSimVarValue('L:A32NX_RESET_PANEL_FMC_B', SimVarValueType.Bool);
    const allFmcInop =
      !SimVar.GetSimVarValue('L:A32NX_FMC_A_IS_HEALTHY', SimVarValueType.Bool) &&
      !SimVar.GetSimVarValue('L:A32NX_FMC_B_IS_HEALTHY', SimVarValueType.Bool) &&
      !SimVar.GetSimVarValue('L:A32NX_FMC_C_IS_HEALTHY', SimVarValueType.Bool);

    this.legacyFmsIsHealthy.set(!allFmcInop);
    if (this.instance !== FmcIndex.FmcA || (this.instance === FmcIndex.FmcA && allFmcInop)) {
      if (this.instance === FmcIndex.FmcA && (allFmcResetsPulled || allFmcInop) && this.wasReset === false) {
        this.reset();
      }
      return;
    }

    this.wasReset = false;

    this.flightPhaseManager.shouldActivateNextPhase(dt);

    const throttledDt = this.fmsUpdateThrottler.canUpdate(dt);

    if (throttledDt !== -1) {
      this.navigation.update(throttledDt);
      if (this.flightPlanService.hasActive) {
        const flightPhase = this.flightPhase.get();
        this.enginesWereStarted.set(
          flightPhase >= FmgcFlightPhase.Takeoff ||
            (flightPhase == FmgcFlightPhase.Preflight && SimVar.GetSimVarValue('L:A32NX_ENGINE_N2:1', 'number') > 20) ||
            SimVar.GetSimVarValue('L:A32NX_ENGINE_N2:2', 'number') > 20 ||
            SimVar.GetSimVarValue('L:A32NX_ENGINE_N2:3', 'number') > 20 ||
            SimVar.GetSimVarValue('L:A32NX_ENGINE_N2:4', 'number') > 20,
        );

        this.acInterface.updateThrustReductionAcceleration();
        this.acInterface.updateTransitionAltitudeLevel();
        this.acInterface.updatePerformanceData();
        this.acInterface.updatePerfSpeeds();
        this.acInterface.updateWeights();
        this.acInterface.toSpeedsChecks();
        this.acInterface.checkForStepClimb();
        this.acInterface.checkTooSteepPath();
        this.acInterface.checkDestEfobBelowMin();
        this.acInterface.checkDestEfobBelowMinScratchPadMessage(throttledDt);
        this.acInterface.checkEngineOut(throttledDt);

        const toFlaps = this.fmgc.getTakeoffFlapsSetting();
        if (toFlaps) {
          this.acInterface.setTakeoffFlaps(toFlaps);
        }

        const thsFor = this.fmgc.data.takeoffThsFor.get();
        if (thsFor) {
          this.acInterface.setTakeoffTrim(thsFor);
        }

        const destPred = this.guidanceController.vnavDriver.getDestinationPrediction();
        if (destPred) {
          this.acInterface.updateDestinationPredictions(destPred);
        } else {
          this.acInterface.resetDestinationPredictions();
        }
        this.acInterface.updateIlsCourse(this.navigation.getNavaidTuner().getMmrRadioTuningStatus(1));
        this.fmgc.data.alternateExists.set(this.flightPlanService.active.alternateDestinationAirport !== undefined);
      } else {
        this.acInterface.resetDestinationPredictions();
        this.fmgc.data.alternateExists.set(false);
        this.fmgc.data.alternateFuelPilotEntry.set(null);
      }
      this.checkZfwParams();
      this.updateMessageQueue();
      this.updateVerticalPath();

      this.acInterface.checkSpeedLimit();
      this.acInterface.thrustReductionAccelerationChecks();
      // TODO port over from legacy code
      // this.updatePerfPageAltPredictions();
    }

    const flightPlanChanged = this.flightPlanService.activeOrTemporary.version !== this.lastFlightPlanVersion;

    if (flightPlanChanged) {
      this.acInterface.updateManagedProfile();
      this.acInterface.updateDestinationData();

      // Update ND plan center, but only if not on F-PLN page. There has to be a better solution though.
      if (this.mfdReference?.uiService.activeUri.get().page !== 'f-pln') {
        this.updateEfisPlanCentre(
          this.mfdReference?.uiService.captOrFo === 'FO' ? 'R' : 'L',
          FlightPlanIndex.Active,
          this.#flightPlanService.active.activeLegIndex,
          this.#flightPlanService.active.activeLegIndex >= this.#flightPlanService.active.allLegs.length,
        );
      }

      this.lastFlightPlanVersion = this.flightPlanService.activeOrTemporary.version;
    }

    this.acInterface.updateAutopilot(dt);

    this.guidanceController.update(dt);

    this.efisSymbolsLeft.update();
    this.efisSymbolsRight.update();

    this.acInterface.arincBusOutputs.forEach((word) => word.writeToSimVarIfDirty());
  }

  updateEfisPlanCentre(
    side: EfisSide,
    planDisplayForPlan: number,
    planDisplayLegIndex: number,
    planDisplayInAltn: boolean,
  ) {
    this.efisInterfaces[side].setNumLegsOnFplnPage(this.flightPlanService.hasTemporary ? 7 : 8);
    this.efisInterfaces[side].setPlanCentre(planDisplayForPlan, planDisplayLegIndex, planDisplayInAltn);
    this.efisInterfaces[side].setSecRelatedPageOpen(planDisplayForPlan >= FlightPlanIndex.FirstSecondary);
  }

  handleFcuAltKnobPushPull(distanceToDestination: number): void {
    this.flightPhaseManager.handleFcuAltKnobPushPull(distanceToDestination);
  }

  handleFcuAltKnobTurn(distanceToDestination: number): void {
    this.flightPhaseManager.handleFcuAltKnobTurn(distanceToDestination);
  }

  handleFcuVSKnob(distanceToDestination: number, onStepClimbDescent: () => void): void {
    this.flightPhaseManager.handleFcuVSKnob(distanceToDestination, onStepClimbDescent);
  }

  handleNewCruiseAltitudeEntered(newCruiseFlightLevel: number): void {
    this.flightPhaseManager.handleNewCruiseAltitudeEntered(newCruiseFlightLevel);
  }

  tryGoInApproachPhase(): void {
    this.flightPhaseManager.tryGoInApproachPhase();
  }

  private updateVerticalPath() {
    // Transmit active vertical geometry
    const predictions = this.guidanceController.vnavDriver.mcduProfile?.waypointPredictions;
    const plan: ReadonlyFlightPlan = this.flightPlanService.active;

    if (!predictions || !this.flightPlanService.hasActive) {
      this.acInterface.transmitVerticalPath([], [], [], [], null);
      return;
    }

    const targetProfile = this.guidanceController.vnavDriver.mcduProfile;
    const descentProfile = this.guidanceController.vnavDriver.descentProfile;
    const actualProfile = this.guidanceController.vnavDriver.ndProfile;

    const targetProfileVd: VerticalPathCheckpoint[] = [];
    const showFromLegIndex = Math.max(0, plan.activeLegIndex - 1);

    if (targetProfile) {
      targetProfile.checkpoints.forEach((c) => {
        const currentDistance = targetProfile.distanceToPresentPosition;

        targetProfileVd.push({
          distanceFromAircraft: c.distanceFromStart - currentDistance,
          altitude: c.altitude,
        });
      });
    }

    // Separately extract all altitude constraints
    const vdAltitudeConstraints: VdAltitudeConstraint[] = plan.allLegs
      .slice(showFromLegIndex)
      .filter((leg, legIndex) => leg.isDiscontinuity === false && predictions.get(legIndex + showFromLegIndex))
      .map((_, legIndex) => {
        const legPrediction = predictions.get(legIndex + showFromLegIndex);

        return {
          altitudeConstraint: legPrediction?.altitudeConstraint,
          isAltitudeConstraintMet: legPrediction?.isAltitudeConstraintMet,
        };
      });

    const descentProfileVd: VerticalPathCheckpoint[] = descentProfile
      ? descentProfile.checkpoints.map((c) => {
          return {
            distanceFromAircraft:
              c.distanceFromStart +
              this.guidanceController.vnavDriver.computeTacticalToGuidanceProfileOffset() -
              descentProfile.distanceToPresentPosition,
            altitude: c.altitude,
            altitudeConstraint: undefined,
            isAltitudeConstraintMet: undefined,
          };
        })
      : [];

    const actualProfileVd: VerticalPathCheckpoint[] = actualProfile
      ? actualProfile.checkpoints.map((c) => {
          return {
            distanceFromAircraft: c.distanceFromStart - actualProfile.distanceToPresentPosition,
            altitude: c.altitude,
            altitudeConstraint: undefined,
            isAltitudeConstraintMet: undefined,
          };
        })
      : [];

    // Start of grey area
    // FIXME improve, currently only works on a per-leg-basis
    let lastBearing: number | null = null;
    let trackChangesSignificantlyAtDistance: number | null = null;
    const previousLeg = plan.allLegs[showFromLegIndex - 1];
    let lastLegLatLong: Coordinates =
      showFromLegIndex > 0 && previousLeg.isDiscontinuity === false && previousLeg.definition.waypoint
        ? previousLeg.definition.waypoint.location
        : this.guidanceController.lnavDriver.ppos;

    plan.allLegs.slice(showFromLegIndex).forEach((leg, legIndex) => {
      const legPrediction = predictions.get(legIndex + showFromLegIndex);
      if (leg.isDiscontinuity === false && legPrediction) {
        if (leg.definition.waypoint && trackChangesSignificantlyAtDistance === null) {
          const bearing = bearingTo(lastLegLatLong, leg.definition.waypoint.location);
          if (lastBearing !== null && Math.abs(lastBearing - bearing) > 3) {
            trackChangesSignificantlyAtDistance = legPrediction.distanceFromAircraft;
          }
          lastBearing = bearing;
        }

        lastLegLatLong = leg.definition.waypoint?.location ?? lastLegLatLong;
      }
    });

    this.acInterface.transmitVerticalPath(
      targetProfileVd,
      vdAltitudeConstraints,
      actualProfileVd,
      descentProfileVd,
      trackChangesSignificantlyAtDistance,
    );
  }

  public enterEngineOut() {
    // Managed speed targets are handled in FmcAircraftInterface.updateManagedSpeed()

    // Update drift-down altitude if in CRZ phase
    // FIXME need to add drift-down PWP to fmsv2

    // Delete pre-selected speeds
    this.fmgc.data.climbPreSelSpeed.set(null);
    this.fmgc.data.cruisePreSelSpeed.set(null);
    this.fmgc.data.cruisePreSelMach.set(null);
    this.fmgc.data.descentPreSelSpeed.set(null);

    // Delete planned/future altitude steps
    this.flightPlanService.active.allLegs
      .filter(
        (l, index) =>
          l.isDiscontinuity === false && index >= this.flightPlanService.active.activeLegIndex && l.cruiseStep,
      )
      .forEach((l) => {
        if (l.isDiscontinuity === false) {
          l.cruiseStep = undefined;
        }
      });

    // Delete time constraints
    // no-op

    // Display PERF page
    this.mfdReference?.uiService.navigateTo('fms/active/perf');
  }

  public exitEngineOut() {
    // Restore long-term guidance targets
  }

  async swapNavDatabase(): Promise<void> {
    await this.reset();
  }

  async reset(): Promise<void> {
    if (this.instance === FmcIndex.FmcA) {
      // FIXME reset ATSU when it is added to A380X
      // this.atsu.resetAtisAutoUpdate();
      this.wasReset = true;
      await this.flightPlanService.reset();
      this.fmgc.data.reset();
      this.initSimVars();
      this.deleteAllStoredWaypoints();
      this.clearLatestFmsErrorMessage();
      this.mfdReference?.uiService.navigateTo('fms/data/status');
      this.navigation.resetState();
    }
  }

  public logTroubleshootingError(msg: any) {
    this.bus.pub('troubleshooting_log_error', String(msg), true, false);
  }

  // TODO refactor in order to transmit message text to the ND. E.g. LEG/AREA/MAN RNP XXX.X
  public sendNdFmMessage(message: FMMessage, side: EfisSide) {
    if (!message.ndFlag) {
      console.warn('FMMessage has no ND flag set, cannot send message', message);
      return;
    }
    const old = this.ndMessageFlags[side];
    this.ndMessageFlags[side] |= message.ndFlag;
    if (this.ndMessageFlags[side] !== old) {
      SimVar.SetSimVarValue(`L:A32NX_EFIS_${side}_ND_FM_MESSAGE_FLAGS`, 'number', this.ndMessageFlags[side]);
    }
  }

  public removeNdFmMessage(message: FMMessage, side: EfisSide) {
    if (!message.ndFlag) {
      console.warn('FMMessage has no ND flag set, cannot recall message', message);
      return;
    }
    const old = this.ndMessageFlags[side];
    this.ndMessageFlags[side] &= ~message.ndFlag;
    if (this.ndMessageFlags[side] !== old) {
      SimVar.SetSimVarValue(`L:A32NX_EFIS_${side}_ND_FM_MESSAGE_FLAGS`, 'number', this.ndMessageFlags[side]);
    }
  }
}
